Skip to content

feat(style): user-defined custom polish styles with editable system prompts#366

Open
katanumahotori wants to merge 2 commits intoOpen-Less:betafrom
katanumahotori:feat/custom-polish-styles
Open

feat(style): user-defined custom polish styles with editable system prompts#366
katanumahotori wants to merge 2 commits intoOpen-Less:betafrom
katanumahotori:feat/custom-polish-styles

Conversation

@katanumahotori
Copy link
Copy Markdown
Contributor

@katanumahotori katanumahotori commented May 8, 2026

User description

Why

The four built-in PolishModes (Raw / Light / Structured / Formal) all ship with hard-coded zh-CN system prompts. Users wanting a 'novel-writing', 'email', or 'casual chat' style had no way to register one, and Ctrl+Shift+S only cycled the four built-ins.

What

Turns PolishMode into a Custom(String) extensible enum (serde transparent via String, so the wire format stays compact) and adds a CRUD surface for user styles:

  • UserPreferences.custom_modes: Vec<CustomMode { id, name, prompt }>
  • add_custom_mode / update_custom_mode / delete_custom_mode IPC commands; delete falls back default_mode to Light if it was pointing at the deleted custom id.
  • prompts::system_prompt(mode, override_text) accepts an override string used by Custom modes (built-ins still render the original ROLE/RULES/OUTPUT scaffolding when override_text is None).
  • Style page: existing 4-mode grid stays, each card gets a 'view default prompt' disclosure that hits the new get_default_polish_prompt IPC. Below it, a new 'Custom styles' section with add / edit / delete UI; each custom mode participates in enabled_modes and the master toggle, and Ctrl+Shift+S cycles through them too.

Custom modes are picked by default_mode and the switch_style_hotkey listener exactly like built-ins, so no other call site needs to learn about them.

Note (scope leakage)

coordinator.rs and Style.tsx in this PR also contain the per-app override logic and UI (next PR's scope). They're bundled because they're the same files; the new Style.tsx 'Per-app override' section needs the AppModeOverride type from this PR's types.rs anyway. If reviewers prefer the override path stripped, that's a follow-up.

Depends on schema in #365 (or this PR can land first and #365 rebases).


PR Type

Enhancement, Tests


Description

  • Add translateEnabled preference plumbing

  • Add custom styles and app overrides

    • Serialize Rust and TypeScript types
    • Validate custom IDs before saving
  • Route custom prompts through polish flow

    • Pick first matching app override
    • Pass custom prompts into composer
  • Expand style UI, locales, and tests

    • Manage custom styles and overrides
    • Add five-language copy and fixtures

Diagram Walkthrough

flowchart LR
  S["Style page"] -- "edits custom styles and overrides" --> I["IPC commands"]
  I -- "persists settings" --> P["UserPreferences (+ translateEnabled)"]
  P -- "selects app-specific mode" --> C["Coordinator"]
  C -- "builds override prompt" --> PR["Polish prompt composer"]
  S -- "uses translated labels" --> L["i18n locales"]
Loading

File Walkthrough

Relevant files
Enhancement
13 files
types.rs
Add custom modes and translate flag                                           
+226/-8 
polish.rs
Support custom prompts in polish flow                                       
+72/-9   
commands.rs
Add translation and custom mode commands                                 
+175/-3 
coordinator.rs
Pick app overrides during dictation                                           
+53/-17 
lib.rs
Register new Tauri commands                                                           
+7/-0     
Style.tsx
Add custom style and override UI                                                 
+693/-74
ja.ts
Add Japanese copy for new settings                                             
+54/-2   
ko.ts
Add Korean copy for new settings                                                 
+52/-0   
zh-TW.ts
Add Traditional Chinese copy for new settings                       
+52/-0   
zh-CN.ts
Add Simplified Chinese copy for new settings                         
+52/-0   
en.ts
Add English copy for new settings                                               
+52/-0   
types.ts
Mirror custom preferences and override types                         
+39/-2   
ipc.ts
Add IPC wrappers for new settings                                               
+37/-0   
Tests
1 files
stylePrefs.test.ts
Update mocked preferences for new fields                                 
+3/-0     

…rompts

The four built-in PolishModes (Raw / Light / Structured / Formal) all
ship with hard-coded zh-CN system prompts. Users wanting a
"novel-writing", "email", or "casual chat" style had no way to register
one, and Ctrl+Shift+S only cycled the four built-ins.

This PR turns PolishMode into a Custom(String) extensible enum (serde
transparent via String, so the wire format stays compact) and adds a
CRUD surface for user styles:

- UserPreferences.custom_modes: Vec<CustomMode { id, name, prompt }>
- add_custom_mode / update_custom_mode / delete_custom_mode IPC
  commands; delete falls back default_mode to Light if it was pointing
  at the deleted custom id.
- prompts::system_prompt(mode, override_text) accepts an override
  string used by Custom modes (built-ins still render the original
  ROLE/RULES/OUTPUT scaffolding when override_text is None).
- Style page: existing 4-mode grid stays, each card gets a "view
  default prompt" disclosure that hits the new
  get_default_polish_prompt IPC. Below it, a new "Custom styles"
  section with add / edit / delete UI; each custom mode participates
  in enabled_modes and the master toggle, and Ctrl+Shift+S cycles
  through them too.

Custom modes are picked by default_mode and the switch_style_hotkey
listener exactly like built-ins, so no other call site needs to learn
about them.

Note: coordinator.rs and Style.tsx in this PR also contain the
per-app override logic and UI (next PR's scope). They are bundled
because they're the same files; the new Style.tsx "Per-app override"
section requires the AppModeOverride type from this PR's types.rs
anyway. If reviewers prefer the override path stripped, that's a
follow-up.
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

katanumahotori pushed a commit to katanumahotori/openless that referenced this pull request May 8, 2026
…Mode::Custom

Without these the PolishMode::Custom variant added to types.rs in this
PR makes the existing match arms in polish.rs / coordinator.rs
non-exhaustive and the crate fails to compile. Same scope-leakage
caveat from the previous commit applies: these will be the canonical
implementations once appergb#366 / appergb#367 land.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

PR Reviewer Guide 🔍

(Review updated until commit 3eab896)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

365 - Partially compliant

Compliant requirements:

  • Add a master Enable translation preference toggle that defaults to true.
  • Persist the toggle through a set_translate_enabled IPC command.

Non-compliant requirements:

  • Add the toggle row and explanatory copy in the Translation settings UI.

Requires further human verification:

  • Verify the translation hotkey registration and overlay suppression behavior in the running app.
  • Verify the new settings row and translated labels render correctly across all supported locales.
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Stale overrides

After deleting a custom style, the page refreshes prefs but leaves the local overrides state unchanged. If any per-app rules referenced that style, the UI can keep showing a now-deleted custom:<id> and the next debounced save can try to persist the stale reference, which the backend will reject. Refresh overrides from the reloaded preferences here as well.

await deleteCustomMode(cm.id);
const fresh = await getSettings();
setPrefs(fresh);
setSaveError(null);

@appergb
Copy link
Copy Markdown
Collaborator

appergb commented May 8, 2026

@katanumahotori Deleting a custom style only removes it from custom_modes, enabled_modes, and default_mode. Any app_mode_overrides entries that still point to that custom ID are retained, so the application might select a mode that no longer exists when matching, ultimately leading to an empty or corrupted path. 覆盖修改仅通过 500 毫秒的防抖机制保存,但卸载清理操作只是清除计时器,而不是刷新它。如果用户更改了应用程序覆盖设置并在该时间窗口内离开页面,则最后一次编辑将被丢弃,并且永远不会被保存。

@katanumahotori
Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. Both issues fixed:

  1. Dangling app_mode_overrides after custom style delete:
    delete_custom_mode now also walks app_mode_overrides and removes any entry whose mode == Custom(deleted_id). The persisted preference is consistent after delete; no app match path can resolve to a non-existent custom id.

  2. Debounced edits dropped on unmount:
    The 500ms debounce in Style.tsx now stores the latest pending override list in a ref. The useEffect cleanup checks the ref and, if there's a pending unsaved value, calls setAppModeOverrides(pending) synchronously before clearing the timer. So leaving the page mid-edit always persists the last keystroke.

Please re-review.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Persistent review updated to latest commit 3eab896

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants